<!DOCTYPE html>
<html>
<head>
  <title>GoJS Packed Class Hierarchy</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <meta name="description" content="The JavaScript class hierarchy defined by the GoJS library, arranged in nested circles." />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="../samples/assets/require.js"></script>
  <script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
    function init() {
      if (window.goSamples) window.goSamples();  // init for these samples -- you don't need to call this

      require(["../extensionsTS/PackedLayout"], function(app) {
        var PackedLayout = app.PackedLayout;

        // subclass PackedLayout to change default properties and override commitLayout function
        function HierarchyLayout() {
          PackedLayout.call(this);
          this.packShape = PackedLayout.Spiral;
          this.hasCircularNodes = true;
          this.sortMode = PackedLayout.Area,
          this.comparer = function(na, nb) {
            // ensure label is placed last
            if (na.data.isLabel) {
              return 1;
            }
            if (nb.data.isLabel) {
              return -1;
            }
            // otherwise sort in ascending order by size (all nodes are circular, so using width or height here doesn't matter)
            return (na.actualBounds.width - nb.actualBounds.width);
          }
        }
        go.Diagram.inherit(HierarchyLayout, PackedLayout);

        /* after each group has had its layout applied, size and position it according
        * to the smallest enclosing circle which goes around all of its nodes */
        HierarchyLayout.prototype.commitLayout = function() {
          if (this.group !== null) {
            var groupData = keyToNodeDataMap.get(this.group.key);

            var enclosingCircle = this.enclosingCircle;
            var actualBounds = this.actualBounds;

            var size = new go.Size(enclosingCircle.width, enclosingCircle.width);
            this.diagram.model.setDataProperty(groupData, "size", size);

            var dx = enclosingCircle.centerX - actualBounds.centerX;
            var dy = enclosingCircle.centerY - actualBounds.centerY;
            var position = new go.Point((actualBounds.width - enclosingCircle.width) / 2 + dx, (actualBounds.height - enclosingCircle.height) / 2 + dy);
            this.diagram.model.setDataProperty(groupData, "position", position);
          }
        }

        var $ = go.GraphObject.make;  // for conciseness in defining templates

        var myDiagram =
          $(go.Diagram, "myDiagramDiv",  // must be the ID or reference to div
            {
              layout: $(HierarchyLayout), // defined above
              "animationManager.isEnabled": false,
              isReadOnly: true,
              initialAutoScale: go.Diagram.Uniform
            });

        // common definitions for both Nodes and Groups
        var toolTipTemplate =
          $(go.Adornment, "Auto",
            $(go.Shape, { fill: "white" }),
            $(go.TextBlock, { margin: 4 },
              new go.Binding("text", "tooltip"))
          );

        var selectionAdornmentTemplate =
          $(go.Adornment, "Auto",
            $(go.Shape, "Circle",
              { fill: null, stroke: "dodgerblue", strokeWidth: 3,
                spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight }),
            $(go.Placeholder)
          );

        function commonStyle() {
          return [
            {
              toolTip: toolTipTemplate,
              selectionAdornmentTemplate: selectionAdornmentTemplate,
              doubleClick: function(e, node) {
                var url = "../../api/symbols/" + node.data.key + ".html";
                window.open(url, "_blank");
              }
            }
          ];
        }

        myDiagram.nodeTemplate =
          $(go.Node, "Auto", commonStyle(),
            new go.Binding("width", "diameter"),
            new go.Binding("height", "diameter"),
            $(go.Shape,
              { figure: "Circle", fill: "#1F4963", strokeWidth: 0, spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight },
              new go.Binding("fill")),
            $(go.TextBlock,
              { font: "12px Helvetica, Arial, sans-serif", stroke: "white", maxLines: 1 },
              new go.Binding("text"),
              new go.Binding("font"),
              new go.Binding("stroke", "fill", function(f) { return go.Brush.isDark(f) ? "white" : "black"; }))
          );

        myDiagram.groupTemplate =
          $(go.Group, commonStyle(),
            { layout: $(HierarchyLayout) }, // defined above
            $(go.Shape, "Circle",
              { fill: "rgba(128,128,128,0.33)" },
              new go.Binding("desiredSize", "size"),
              new go.Binding("position")),
            $(go.Placeholder)  // represents area for all member parts
          );

        // Collect all of the data for the model of the class hierarchy
        var nodeDataArray = [
          { key: "GoJS", text: "GoJS", children: [] },
          // large label to be placed at the very end
          {
            key: "GoJS label",
            group: "GoJS",
            text: "GoJS",
            tooltip: "GoJS",
            fill: null,
            font: "64px Helvetica, bold Arial, sans-serif",
            isLabel: true,
            children: []
          }
        ];

        var keyToNodeDataMap = new go.Map();
        keyToNodeDataMap.add("GoJS", nodeDataArray[0]);

        // Iterate over all of the classes in "go"
        for (var k in go) {
          var cls = go[k];
          if (!cls) continue;
          var proto = cls.prototype;
          if (!proto) continue;
          proto.constructor.className = k;  // remember name

          var propCount = 0;  // count number of properties on the class
          for (var prop in proto) {
            if (proto.hasOwnProperty(prop)) {
              propCount++;
            }
          }

          var data = { key: k, text: k, propCount: propCount, children: [] };
          if (keyToNodeDataMap.has(k)) {
            data.children = keyToNodeDataMap.get(k).children;
          }
          keyToNodeDataMap.add(k, data); // will replace existing key if there is one

          // find base class constructor
          var base = Object.getPrototypeOf(proto).constructor;
          if (base === Object) {  // "root" node?
            data.group = "GoJS";
            keyToNodeDataMap.get("GoJS").children.push(data);
            nodeDataArray.push(data);
          } else {
            // add a node for this class and set its group to its parent
            data.group = base.className;
            if (keyToNodeDataMap.has(base.className)) {
              keyToNodeDataMap.get(base.className).children.push(data);
            } else {
              keyToNodeDataMap.add(base.className, {children: [data]});
            }

            nodeDataArray.push(data);
          }
        }

        // create groups and add labels to groups with only 1 child
        for (var i = nodeDataArray.length - 1; i >= 0; i--) {
          var n = nodeDataArray[i];
          if (n.children.length > 0) {
            n.isGroup = true;
          }

          // add tooltip and/or size node using the total number of properties it has and has inherited
          var totalCount = n.propCount;
          var parentKey = n.group;
          var parentData;
          while ((parentData = keyToNodeDataMap.get(parentKey)) !== null && parentData.propCount !== undefined) {
            totalCount += parentData.propCount;
            parentKey = parentData.group;
          }

          if (totalCount === undefined) { // applies to the root GoJS group only
            n.tooltip = n.text;
          } else {
            n.tooltip = n.text + ": " + totalCount; // add tooltip
          }
          if (!n.isGroup && !n.isLabel) { // only set size of node if it is not a group
            // calculate size by scaling totalCount logarithmically to produce more visually appealing results
            n.diameter = 20 * Math.log(0.5 * (totalCount + 5.5));
          }

          // add label to groups that only have one child
          if (n.children.length === 1) {
            nodeDataArray.push({
              text: n.text,
              tooltip: n.tooltip,
              group: n.key,
              fill: null,
              font: "20px Helvetica, bold Arial, sans-serif"
            });
          }

          delete n.children;
        }

        myDiagram.model = new go.GraphLinksModel(nodeDataArray);
      });
    }
  </script>
</head>
<body onload="init()">
  <div id="sample">
    <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:700px"></div>
    <p>
      Circle packing can be a useful way to visualize hierarchical data, as demonstrated here
      with a visualization of the class hierarchy of the GoJS library. This layout is performed
      automatically by the <a href="../extensions/PackedLayout.html">PackedLayout</a> extension. Nodes
      are sized according to how many properties their corresponding class has, or has inherited.
      As a result, larger nodes generally represent more complex classes. Mouse over nodes to see
      their full name and the number of properties on their corresponding class.
    </p>
    <p>
      This sample is very similar to the <a href="../samples/classHierarchy.html" target="_blank">Class Hierarchy</a> sample,
      except that instead of showing the class hierarchy as a tree, it is displayed using nested circles.
      Opening the API page is achieved by double-clicking on a node, rather than using a "HyperlinkText".
    </p>
  </div>
</body>
</html>